﻿using KeraLua;
using QLua.Exceptions;
using QLua.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using LuaNativeFunction = KeraLua.LuaFunction;
using LuaState = KeraLua.Lua;

namespace QLua.Method;


delegate object ExtractValue(LuaState luaState, int stackPos);
class LuaMethodWrapper
{
	internal LuaNativeFunction InvokeFunction;

	readonly ObjectTranslator _translator;
	readonly MethodBase _method;

	readonly ExtractValue _extractTarget;
	readonly object _target;
	readonly bool _isStatic;

	readonly string _methodName;
	readonly MethodInfo[] _members;

	private MethodCache _lastCalledMethod;


	/*
	 * Constructs the wrapper for a known MethodBase instance
	 */
	public LuaMethodWrapper(ObjectTranslator translator, object target, ProxyType targetType, MethodBase method)
	{
		InvokeFunction = Call;
		_translator = translator;
		_target = target;
		_extractTarget = translator.typeChecker.GetExtractor(targetType);
		_lastCalledMethod = new MethodCache();

		_method = method;
		_methodName = method.Name;
		_isStatic = method.IsStatic;

	}

	/*
	 * Constructs the wrapper for a known method name
	 */
	public LuaMethodWrapper(ObjectTranslator translator, ProxyType targetType, string methodName, BindingFlags bindingType)
	{
		InvokeFunction = Call;

		_translator = translator;
		_methodName = methodName;
		_extractTarget = translator.typeChecker.GetExtractor(targetType);
		_lastCalledMethod = new MethodCache();

		_isStatic = (bindingType & BindingFlags.Static) == BindingFlags.Static;
		MethodInfo[] methods = GetMethodsRecursively(targetType.UnderlyingSystemType,
			methodName,
			bindingType | BindingFlags.Public);
		_members = ReorderMethods(methods);
	}

	private static MethodInfo[] ReorderMethods(MethodInfo[] m)
	{
		int len = m.Length;

		if (len < 2)
			return m;

		return m.
			GroupBy(c => c.GetParameters().Length).
			SelectMany(g => g.OrderByDescending(ci => ci.ToString())).
			ToArray();
	}

	MethodInfo[] GetMethodsRecursively(Type type, string methodName, BindingFlags bindingType)
	{
		if (type == typeof(object))
			return type.GetMethods(methodName, bindingType);

		var methods = LuaMemberAttribute.GetMethodsForType(type, methodName, bindingType);
		var baseMethods = GetMethodsRecursively(type.BaseType, methodName, bindingType);

		return methods.Concat(baseMethods).ToArray();
	}

	/// <summary>
	/// Convert C# exceptions into Lua errors
	/// </summary>
	/// <returns>num of things on stack</returns>
	/// <param name="e">null for no pending exception</param>
	int SetPendingException(Exception e)
	{
		return _translator.Interpreter?.SetPendingException(e) ?? 0;
	}

	void FillMethodArguments(LuaState luaState, int numStackToSkip)
	{
		object[] args = _lastCalledMethod.args;


		for (int i = 0; i < _lastCalledMethod.argTypes.Length; i++)
		{
			MethodArgs type = _lastCalledMethod.argTypes[i];

			int index = i + 1 + numStackToSkip;


			if (_lastCalledMethod.argTypes[i].IsParamsArray)
			{
				int count = _lastCalledMethod.argTypes.Length - i;
				Array paramArray = _translator.CreateParamsArray(luaState, type.ExtractValue, type.ParameterType, index, count);
				args[_lastCalledMethod.argTypes[i].Index] = paramArray;
			}
			else
			{
				args[type.Index] = type.ExtractValue(luaState, index);
			}

			if (_lastCalledMethod.args[_lastCalledMethod.argTypes[i].Index] == null &&
					!luaState.IsNil(i + 1 + numStackToSkip))
				throw new LuaException(string.Format("Argument number {0} is invalid", (i + 1)));
		}
	}

	/*
	 * Pushes all return values onto the stack (including out and ref parameters).
	 * Returns the number of return values.
	 */
	int PushReturnValue(LuaState luaState, object returnValue)
	{
		int nReturnValues = 0;
		if (!_lastCalledMethod.IsReturnVoid)
			nReturnValues = _translator.PushMultiple(luaState, returnValue);

		// Pushes out and ref return values
		for (int index = 0; index < _lastCalledMethod.outList.Length; index++)
		{
			nReturnValues++;
			_translator.Push(luaState, _lastCalledMethod.args[_lastCalledMethod.outList[index]]);
		}

		return nReturnValues;
	}

	int CallInvoke(LuaState luaState, MethodBase method, object targetObject)
	{
		if (!luaState.CheckStack(_lastCalledMethod.outList.Length + 6))
			throw new LuaException("Lua stack overflow");

		try
		{
			object result;

			if (method.IsConstructor)
				result = ((ConstructorInfo)method).Invoke(_lastCalledMethod.args);
			else
				result = method.Invoke(targetObject, _lastCalledMethod.args);

			return PushReturnValue(luaState, result);
		}
		catch (TargetInvocationException e)
		{
			// Failure of method invocation
			Lua interpreter = _translator.Interpreter;
			if (interpreter?.UseTraceback is true)
				e.GetBaseException().Data["Traceback"] = interpreter.GetDebugTraceback();
			return SetPendingException(e.GetBaseException());
		}
		catch (Exception e)
		{
			return SetPendingException(e);
		}
	}

	bool IsMethodCached(LuaState luaState, int numArgsPassed, int skipParams)
	{
		if (_lastCalledMethod.cachedMethod == null)
			return false;

		if (numArgsPassed != _lastCalledMethod.argTypes.Length)
			return false;

		// If there is no method overloads, is ok to use the cached method
		if (_members.Length == 1)
			return true;

		return _translator.MatchParameters(luaState, _lastCalledMethod.cachedMethod, _lastCalledMethod, skipParams);
	}

	int CallMethodFromName(LuaState luaState)
	{
		object targetObject = null;

		if (!_isStatic)
			targetObject = _extractTarget(luaState, 1);

		int numStackToSkip =
			_isStatic
				? 0
				: 1; // If this is an instance invoe we will have an extra arg on the stack for the targetObject
		int numArgsPassed = luaState.GetTop() - numStackToSkip;

		// Cached?
		if (IsMethodCached(luaState, numArgsPassed, numStackToSkip))
		{
			MethodBase method = _lastCalledMethod.cachedMethod;

			if (!luaState.CheckStack(_lastCalledMethod.outList.Length + 6))
				throw new LuaException("Lua stack overflow");

			FillMethodArguments(luaState, numStackToSkip);

			return CallInvoke(luaState, method, targetObject);
		}

		// If we are running an instance variable, we can now pop the targetObject from the stack
		if (!_isStatic)
		{
			if (targetObject == null)
			{
				_translator.ThrowError(luaState,
					string.Format("instance method '{0}' requires a non null target object", _methodName));
				return 1;
			}

			luaState.Remove(1); // Pops the receiver
		}

		bool hasMatch = false;
		string candidateName = null;

		foreach (var member in _members)
		{
			if (member.ReflectedType == null)
				continue;

			candidateName = member.ReflectedType.Name + "." + member.Name;
			bool isMethod = _translator.MatchParameters(luaState, member, _lastCalledMethod, 0);

			if (isMethod)
			{
				hasMatch = true;
				break;
			}
		}

		if (!hasMatch)
		{
			string msg = (candidateName == null)
				? "Invalid arguments to method call"
				: ("Invalid arguments to method: " + candidateName);
			_translator.ThrowError(luaState, msg);
			return 1;
		}

		if (_lastCalledMethod.cachedMethod.ContainsGenericParameters)
			return CallInvokeOnGenericMethod(luaState, (MethodInfo)_lastCalledMethod.cachedMethod, targetObject);

		return CallInvoke(luaState, _lastCalledMethod.cachedMethod, targetObject);
	}

	int CallInvokeOnGenericMethod(LuaState luaState, MethodInfo methodToCall, object targetObject)
	{
		//need to make a concrete type of the generic method definition
		Dictionary<string, Type> genericParameterNames = new Dictionary<string, Type>();

		ParameterInfo[] parameters = methodToCall.GetParameters();
		for (int i = 0; i < parameters.Length; i++)
		{
			ParameterInfo parameter = parameters[i];

			if (parameter.ParameterType.IsGenericType)
			{
				var currentArg = _lastCalledMethod.args[i];
				var currentArgType = currentArg.GetType();
				var genericArgTypeArguments = currentArgType.GenericTypeArguments;

				//if currentArgType is array, use element type to be generic arguments for compatibility IEnumerable.
				if (currentArgType.IsArray)
				{
					genericArgTypeArguments = new Type[] { currentArgType.GetElementType() };
				}

				var genericTypeArguments = parameter.ParameterType.GenericTypeArguments;
				for (int j = 0; j < genericTypeArguments.Length; j++)
				{
					var arg = genericTypeArguments[j];
					if (arg.IsGenericParameter && !genericParameterNames.ContainsKey(arg.Name))
					{
						genericParameterNames.Add(arg.Name, genericArgTypeArguments[j]);
					}
				}
			}

			if (parameter.ParameterType.IsGenericParameter)
			{
				genericParameterNames.Add(parameter.ParameterType.Name, _lastCalledMethod.args[i].GetType());
			}
		}

		//Map Name/Type to generic arguments types
		var methodGenericArguments = methodToCall.GetGenericArguments();
		var typeArgs = methodGenericArguments.Select(item => genericParameterNames[item.Name]);

		MethodInfo concreteMethod = methodToCall.MakeGenericMethod(typeArgs.ToArray());
		var result = concreteMethod.Invoke(targetObject, _lastCalledMethod.args);

		return PushReturnValue(luaState, result);
	}

	/*
	 * Calls the method. Receives the arguments from the Lua stack
	 * and returns values in it.
	 */
	int Call(IntPtr state)
	{
		var luaState = LuaState.FromIntPtr(state);

		MethodBase methodToCall = _method;
		object targetObject = _target;

		if (!luaState.CheckStack(5))
			throw new LuaException("Lua stack overflow");

		SetPendingException(null);

		// Method from name
		if (methodToCall == null)
			return CallMethodFromName(luaState);

		// Method from MethodBase instance
		if (!methodToCall.ContainsGenericParameters)
		{
			if (!methodToCall.IsStatic && !methodToCall.IsConstructor && targetObject == null)
			{
				targetObject = _extractTarget(luaState, 1);
				luaState.Remove(1); // Pops the receiver
			}

			if (!_translator.MatchParameters(luaState, methodToCall, _lastCalledMethod, 0))
			{
				_translator.ThrowError(luaState, "Invalid arguments to method call");
				return 1;
			}
		}
		else
		{
			if (!methodToCall.IsGenericMethodDefinition)
			{
				_translator.ThrowError(luaState,
					"Unable to invoke method on generic class as the current method is an open generic method");
				return 1;
			}

			_translator.MatchParameters(luaState, methodToCall, _lastCalledMethod, 0);

			return CallInvokeOnGenericMethod(luaState, (MethodInfo)methodToCall, targetObject);
		}

		if (_isStatic)
			targetObject = null;

		return CallInvoke(luaState, _lastCalledMethod.cachedMethod, targetObject);
	}
}
